page.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. 'use client';
  2. import { use, useEffect, useRef, useState } from 'react';
  3. import { useDonationHub } from '@/hooks/useDonationHub';
  4. import { fetchApi } from '@/lib/utils/client';
  5. import { GoalProgress } from '@/types/donation';
  6. import './style.scss';
  7. type Props = {
  8. params: Promise<{ widgetToken: string }>;
  9. searchParams: Promise<{ [key: string]: string|string[]|undefined }>;
  10. };
  11. type GoalStyleConfig = {
  12. id: number;
  13. title: string;
  14. style: number;
  15. startAmount: number;
  16. targetAmount: number;
  17. isShowPercent: boolean;
  18. barColor: string;
  19. barBackgroundColor: string;
  20. barHeightPx: number;
  21. titleFontSizePx: number;
  22. titleFontColor: string;
  23. amountFontSizePx: number;
  24. amountFontColor: string;
  25. titleFontFamily: string|null;
  26. amountFontFamily: string|null;
  27. isActive: boolean;
  28. };
  29. const DEFAULT_STYLE: Omit<GoalStyleConfig, 'id'|'title'|'style'|'startAmount'|'targetAmount'|'isActive'> = {
  30. isShowPercent: true,
  31. barColor: '#FF6B35',
  32. barBackgroundColor: '#333333',
  33. barHeightPx: 30,
  34. titleFontSizePx: 18,
  35. titleFontColor: '#FFFFFF',
  36. amountFontSizePx: 14,
  37. amountFontColor: '#CCCCCC',
  38. titleFontFamily: null,
  39. amountFontFamily: null
  40. };
  41. export default function GoalPage({ params, searchParams }: Props) {
  42. const { widgetToken } = use(params);
  43. const sp = use(searchParams);
  44. const configID = typeof sp.configID === 'string' ? parseInt(sp.configID, 10) : null;
  45. const hubUrl = process.env.NEXT_PUBLIC_API_URL + '/hubs/donation';
  46. const { goalProgress, setGoalProgress } = useDonationHub(widgetToken, hubUrl);
  47. const [styleCfg, setStyleCfg] = useState<GoalStyleConfig|null>(null);
  48. const widgetRef = useRef<HTMLDivElement>(null);
  49. // 스타일 config 로드
  50. useEffect(() => {
  51. const qs = configID ? `?configID=${configID}` : '';
  52. fetchApi<GoalStyleConfig>(`/api/widget/goal/config/${widgetToken}${qs}`, { silent: true }).then(res => {
  53. if (res.data) {
  54. setStyleCfg(res.data);
  55. }
  56. }).catch(() => {});
  57. }, [widgetToken, configID]);
  58. // 진행률 데이터 로드 (SignalR broadcast 전 초기 표시)
  59. useEffect(() => {
  60. const qs = configID ? `?configID=${configID}` : '';
  61. fetchApi<GoalProgress>(`/api/widget/goal/by-token/${widgetToken}${qs}`, { silent: true }).then(res => {
  62. if (res.data) {
  63. setGoalProgress(res.data);
  64. }
  65. }).catch(() => {});
  66. }, [widgetToken, configID, setGoalProgress]);
  67. // CSS variables 적용 (goalProgress 없어도 styleCfg 기반으로 빈 진행바 스타일링)
  68. useEffect(() => {
  69. if (!widgetRef.current) {
  70. return;
  71. }
  72. const el = widgetRef.current;
  73. const cfg = styleCfg ?? null;
  74. const titleFontSize = cfg?.titleFontSizePx ?? DEFAULT_STYLE.titleFontSizePx;
  75. const titleColor = cfg?.titleFontColor ?? DEFAULT_STYLE.titleFontColor;
  76. const barHeight = cfg?.barHeightPx ?? DEFAULT_STYLE.barHeightPx;
  77. const barBgColor = cfg?.barBackgroundColor ?? DEFAULT_STYLE.barBackgroundColor;
  78. const barColor = cfg?.barColor ?? DEFAULT_STYLE.barColor;
  79. const amountSize = cfg?.amountFontSizePx ?? DEFAULT_STYLE.amountFontSizePx;
  80. const amountColor = cfg?.amountFontColor ?? DEFAULT_STYLE.amountFontColor;
  81. const titleFamily = cfg?.titleFontFamily ?? 'inherit';
  82. const amountFamily = cfg?.amountFontFamily ?? 'inherit';
  83. const fillPercent = goalProgress ? Math.min(goalProgress.percent, 100) : 0;
  84. el.style.setProperty('--goal-title-font-size', `${titleFontSize}px`);
  85. el.style.setProperty('--goal-title-color', titleColor);
  86. el.style.setProperty('--goal-title-font-family', titleFamily);
  87. el.style.setProperty('--goal-bar-height', `${barHeight}px`);
  88. el.style.setProperty('--goal-bar-bg-color', barBgColor);
  89. el.style.setProperty('--goal-bar-color', barColor);
  90. el.style.setProperty('--goal-bar-fill-width', `${fillPercent}%`);
  91. el.style.setProperty('--goal-amount-font-size', `${amountSize}px`);
  92. el.style.setProperty('--goal-amount-color', amountColor);
  93. el.style.setProperty('--goal-amount-font-family', amountFamily);
  94. }, [goalProgress, styleCfg]);
  95. // goalProgress 없어도 styleCfg 기반으로 빈 진행바 표시 (위젯 즉시 노출)
  96. const display = goalProgress ?? {
  97. title: styleCfg?.title ?? '후원 목표',
  98. startAmount: styleCfg?.startAmount ?? 0,
  99. targetAmount: styleCfg?.targetAmount ?? 0,
  100. currentAmount: styleCfg?.startAmount ?? 0,
  101. percent: 0
  102. };
  103. const { title, currentAmount, targetAmount, percent } = display;
  104. const isShowPercent = styleCfg?.isShowPercent ?? DEFAULT_STYLE.isShowPercent;
  105. return (
  106. <div ref={widgetRef} className="goal-widget">
  107. <div className="goal-title">
  108. {title}
  109. </div>
  110. <div className="goal-bar-wrapper">
  111. <div className="goal-bar-bg">
  112. <div className="goal-bar-fill" />
  113. </div>
  114. <div className="goal-bar-text">
  115. {currentAmount.toLocaleString()}원 / {targetAmount.toLocaleString()}원
  116. {isShowPercent && ` (${percent}%)`}
  117. </div>
  118. </div>
  119. </div>
  120. );
  121. }